//-----------------------------------------------------------------------
// <copyright file="EventTopic.cs" company="iron9light">
//      Copyright (c) iron9light. All rights reserved.
// </copyright>
// <author>iron9light</author>
//-----------------------------------------------------------------------

namespace EventBroker
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;

    /// <summary>
    /// Represents the method that will handle an event topic fire.
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">An <see cref="EventArgs"/> that contains the event data.</param>
    /// <param name="exceptions">The exception collection.</param>
    public delegate void EventTopicFireHandler(object sender, EventArgs e, ICollection<Exception> exceptions);

    /// <summary>
    /// Represents a point of communication on a certain topic between the topic publishers and the topic subscribers.
    /// </summary>
    public class EventTopic : IEventTopic
    {
        private readonly string name;
        private readonly List<IPublication> publications = new List<IPublication>();
        private readonly List<ISubscription> subscriptions = new List<ISubscription>();

        /// <summary>
        /// Initializes a new instance of the <see cref="EventTopic"/> class.
        /// </summary>
        /// <param name="name">The topic name.</param>
        public EventTopic(string name)
        {
            this.name = name;
        }

        /// <summary>
        /// Gets the topic name.
        /// </summary>
        public string Name
        {
            get { return this.name; }
        }

        /// <summary>
        /// Gets the count of registered publications with this <see cref="EventTopic"/>.
        /// </summary>
        public virtual int PublicationCount
        {
            get
            {
                this.Clean();
                return this.publications.Count;
            }
        }

        /// <summary>
        /// Gets the count of registered subscriptions to this <see cref="EventTopic"/>.
        /// </summary>
        public virtual int SubscriptionCount
        {
            get
            {
                this.Clean();
                return this.subscriptions.Count;
            }
        }

        /// <summary>
        /// Adds a publication to the topic.
        /// </summary>
        /// <param name="publication">The publication.</param>
        public void AddPublication(IPublication publication)
        {
            Guard.ArgumentNotNull(publication, "publication");

            this.Clean();

            this.ThrowIfRepeatedPublication(publication.Publisher, publication.EventName);

            publication.EventTopic = this;
            this.publications.Add(publication);
        }

        /*
        /// <summary>
        /// Adds a publication to the topic.
        /// </summary>
        /// <param name="publisher">The object that publishes the event that will fire the topic.</param>
        /// <param name="eventName">The name of the event.</param>
        public virtual void AddPublication(object publisher, string eventName)
        {
            Guard.ArgumentNotNull(publisher, "publisher");
            Guard.ArgumentNotNullOrEmptyString(eventName, "eventName");

            IPublication publication = new Publication(
                    this,
                    publisher,
                    eventName);

            this.AddPublication(publication);
        }
        */

        /////// <summary>
        /////// Checks if the specified publication has been registered with this <see cref="EventTopic"/>.
        /////// </summary>
        /////// <param name="publisher">The object that contains the publication.</param>
        /////// <param name="eventName">The name of event on the publisher that fires the topic.</param>
        /////// <returns>true if the topic contains the requested publication; otherwise false.</returns>
        ////public virtual bool ContainsPublication(object publisher, string eventName)
        ////{
        ////    Guard.ArgumentNotNull(
        ////            publisher,
        ////            "publisher");
        ////    Guard.ArgumentNotNull(
        ////            eventName,
        ////            "eventName");
        ////    this.Clean();
        ////    return this.FindPublication(
        ////                   publisher,
        ////                   eventName) != null;
        ////}

        /// <summary>
        /// Removes a publication from this <see cref="EventTopic"/>.
        /// </summary>
        /// <param name="publisher">The object that contains the publication.</param>
        /// <param name="eventName">The name of event on the publisher that fires the topic.</param>
        public virtual void RemovePublication(object publisher, string eventName)
        {
            Guard.ArgumentNotNull(
                    publisher,
                    "publisher");
            Guard.ArgumentNotNull(
                    eventName,
                    "eventName");
            this.Clean();
            IPublication publication = this.FindPublication(
                    publisher,
                    eventName);
            if (publication != null)
            {
                this.publications.Remove(publication);
                publication.Dispose();
            }
        }

        /// <summary>
        /// Removes all publications of the publisher from this <see cref="EventTopic"/>.
        /// </summary>
        /// <param name="publisher">The object that contains the publication.</param>
        public virtual void RemovePublisher(object publisher)
        {
            Guard.ArgumentNotNull(publisher, "publisher");

            this.Clean();

            List<IPublication> publicationList =
                    this.publications.FindAll(delegate(IPublication match) { return match.Publisher == publisher; });

            foreach (Publication publication in publicationList)
            {
                this.publications.Remove(publication);
                publication.Dispose();
            }
        }

        /// <summary>
        /// Adds a subcription to this <see cref="EventTopic"/>.
        /// </summary>
        /// <param name="subscription">The subscription.</param>
        public void AddSubscription(ISubscription subscription)
        {
            Guard.ArgumentNotNull(subscription, "subscription");

            this.Clean();

            if (this.FindSubscription(subscription.Subscriber, subscription.HandlerMethodName) == null)
            {
                this.subscriptions.Add(subscription);
            }
        }

        /*
        /// <summary>
        /// Adds a subcription to this <see cref="EventTopic"/>.
        /// </summary>
        /// <param name="subscriber">The object that contains the method that will handle the <see cref="EventTopic"/>.</param>
        /// <param name="handlerMethodName">The name of the method on the subscriber that will handle the <see cref="EventTopic"/>.</param>
        public virtual void AddSubscription(object subscriber, string handlerMethodName)
        {
            this.AddSubscription(
                    subscriber,
                    handlerMethodName,
                    null);
        }

        /// <summary>
        /// Adds a subcription to this <see cref="EventTopic"/>.
        /// </summary>
        /// <param name="subscriber">The object that contains the method that will handle the <see cref="EventTopic"/>.</param>
        /// <param name="handlerMethodName">The name of the method on the subscriber that will handle the <see cref="EventTopic"/>.</param>
        /// <param name="parameterTypes">Defines the types and order of the parameters for the subscriber. For none pass null.
        /// Use this overload when there are several methods with the same name on the subscriber.</param>
        public virtual void AddSubscription(object subscriber, string handlerMethodName, Type[] parameterTypes)
        {
            Guard.ArgumentNotNull(
                    subscriber,
                    "subscriber");
            Guard.ArgumentNotNullOrEmptyString(
                    handlerMethodName,
                    "handlerMethodName");

            ISubscription subscription = new Subscription(subscriber, handlerMethodName, parameterTypes);

            this.AddSubscription(subscription);
        }
        */

        /// <summary>
        /// Removes a subscription from this <see cref="EventTopic"/>.
        /// </summary>
        /// <param name="subscriber">The object that contains the method that will handle the <see cref="EventTopic"/>.</param>
        /// <param name="handlerMethodName">The name of the method on the subscriber that will handle the <see cref="EventTopic"/>.</param>
        public virtual void RemoveSubscription(object subscriber, string handlerMethodName)
        {
            Guard.ArgumentNotNull(
                    subscriber,
                    "subscriber");
            Guard.ArgumentNotNull(
                    handlerMethodName,
                    "handlerMethodName");
            this.Clean();
            ISubscription subscription = this.FindSubscription(subscriber, handlerMethodName);
            if (subscription != null)
            {
                this.subscriptions.Remove(subscription);
            }
        }

        /// <summary>
        /// Removes all subscriptions of the subscriber from this <see cref="EventTopic"/>.
        /// </summary>
        /// <param name="subscriber">The object that contains the method that will handle the <see cref="EventTopic"/>.</param>
        public virtual void RemoveSubscription(object subscriber)
        {
            Guard.ArgumentNotNull(subscriber, "subscriber");

            this.Clean();

            this.subscriptions.RemoveAll(
                    delegate(ISubscription match)
                    {
                        return match.Subscriber == subscriber;
                    });
        }

        /////// <summary>
        /////// Checks if the specified subscription has been registered with this <see cref="EventTopic"/>.
        /////// </summary>
        /////// <param name="subscriber">The object that contains the method that will handle the <see cref="EventTopic"/>.</param>
        /////// <param name="handlerMethodName">The name of the method on the subscriber that will handle the <see cref="EventTopic"/>.</param>
        /////// <param name="parameterTypes">The parameter types.</param>
        /////// <returns>
        /////// true, if the topic contains the subscription; otherwise false.
        /////// </returns>
        ////public virtual bool ContainsSubscription(object subscriber, string handlerMethodName, Type[] parameterTypes)
        ////{
        ////    Guard.ArgumentNotNull(
        ////            subscriber,
        ////            "subscriber");
        ////    Guard.ArgumentNotNull(
        ////            handlerMethodName,
        ////            "handlerMethodName");
        ////    this.Clean();
        ////    return this.FindSubscription(
        ////                   subscriber,
        ////                   handlerMethodName) != null;
        ////}

        /// <summary>
        /// Called to free resources.
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Fires the <see cref="EventTopic"/>.
        /// </summary>
        /// <param name="sender">The object that acts as the sender of the event to the subscribers.</param>
        /// <param name="e">An <see cref="EventArgs"/> instance to be passed to the subscribers.</param>
        public virtual void Fire(object sender, EventArgs e)
        {
            this.Clean();

            this.CallSubscriptionHandlers(
                    sender,
                    e,
                    this.GetAllHandlers());
        }

        /// <summary>
        /// Fires the specified publication.
        /// </summary>
        /// <param name="publication">The publication.</param>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        public void Fire(IPublication publication, object sender, EventArgs e)
        {
            this.Fire(
                    sender,
                    e);
        }

        /// <summary>
        /// Called to free resources.
        /// </summary>
        /// <param name="disposing">Should be true when calling from Dispose().</param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                foreach (Publication publication in this.publications)
                {
                    publication.Dispose();
                }

                this.publications.Clear();
                this.subscriptions.Clear();
            }
        }

        /// <summary>
        /// Perform a sanity cleaning of the dead references to publishers and subscribers
        /// </summary>
        /// <devdoc>As the topic maintains <see cref="WeakReference"/> to publishers and subscribers,
        /// those instances that are finished but hadn't been removed from the topic will leak. This method
        /// deals with that case.</devdoc>
        private void Clean()
        {
            foreach (Subscription subscription in this.subscriptions.ToArray())
            {
                if (subscription.Subscriber == null)
                {
                    this.subscriptions.Remove(subscription);
                }
            }

            foreach (Publication publication in this.publications.ToArray())
            {
                if (publication.Publisher == null)
                {
                    this.publications.Remove(publication);
                    publication.Dispose();
                }
            }
        }

        /// <summary>
        /// Finds the publication.
        /// </summary>
        /// <param name="publisher">The publisher.</param>
        /// <param name="eventName">Name of the event.</param>
        /// <returns>The publication.</returns>
        private IPublication FindPublication(object publisher, string eventName)
        {
            IPublication publication =
                    this.publications.Find(
                            delegate(IPublication match) { return match.Publisher == publisher && match.EventName == eventName; });
            return publication;
        }

        /// <summary>
        /// Finds the subscription.
        /// </summary>
        /// <param name="subscriber">The subscriber.</param>
        /// <param name="handlerMethodName">Name of the handler method.</param>
        /// <returns>The subscription.</returns>
        private ISubscription FindSubscription(object subscriber, string handlerMethodName)
        {
            ISubscription subscription = this.subscriptions.Find(
                    delegate(ISubscription match)
                    {
                        return match.Subscriber == subscriber && match.HandlerMethodName == handlerMethodName;
                    });
            return subscription;
        }

        /// <summary>
        /// Calls the subscription handlers.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        /// <param name="handlers">The handlers.</param>
        private void CallSubscriptionHandlers(object sender, EventArgs e, IEnumerable<EventTopicFireHandler> handlers)
        {
            List<Exception> exceptions = new List<Exception>();

            foreach (EventTopicFireHandler handler in handlers)
            {
                handler(
                        sender,
                        e,
                        exceptions);
            }

            if (exceptions.Count > 0)
            {
                throw new EventTopicException(
                        this,
                        new ReadOnlyCollection<Exception>(exceptions));
            }
        }

        /// <summary>
        /// Gets all handlers.
        /// </summary>
        /// <returns>The handlers.</returns>
        private IEnumerable<EventTopicFireHandler> GetAllHandlers()
        {
            List<EventTopicFireHandler> handlers = new List<EventTopicFireHandler>();

            foreach (Subscription subscription in this.subscriptions)
            {
                handlers.Add(subscription.Handler);
            }

            return handlers;
        }

        /// <summary>
        /// Throws if repeated publication.
        /// </summary>
        /// <param name="publisher">The publisher.</param>
        /// <param name="eventName">Name of the event.</param>
        private void ThrowIfRepeatedPublication(object publisher, string eventName)
        {
            if (this.FindPublication(
                        publisher,
                        eventName) != null)
            {
                throw new EventBrokerException(Properties.Resources.OnlyOnePublicationIsAllowed);
            }
        }
    }
}